use crate::{
    coordinate::Coord,
    direction::Dir,
    layout::{Builder, Layout},
};
use derive_more::Deref;
use std::rc::Rc;

#[derive(Clone, Debug, PartialEq, Eq, Deref)]
pub struct State {
    #[deref]
    layout: Rc<Layout>,
    cells: Box<[Dir]>,
}

impl State {
    /// 获取一格
    ///
    /// 它**不**返回引用。
    /// 因为复制一个值的代价更低，而且允许内部优化，如 Bit Vector。
    pub fn get(&self, coord: Coord) -> Option<Dir> {
        self.cells.get(self.get_index(coord)?).copied()
    }

    /// 修改一格
    ///
    /// 如果格子有东西，则修改，并返回旧值。
    /// 如果格子为空，则不修改，并返回[`None`]。
    ///
    /// 也就是说，无法在空格子里放东西，即，无法将[`None`]改为[`Some`]。
    /// 这没有问题：盘面布局不应改变。
    ///
    /// 之所以不返回`&mut`而是直接设置，是为了允许内部优化，如 Bit Vector。
    pub fn set(&mut self, coord: Coord, dir: Dir) -> Option<Dir> {
        let dest = self.cells.get_mut(self.get_index(coord)?)?;
        Some(std::mem::replace(dest, dir))
    }

    /// 顺时针旋转一格，并返回其旋转后指向的坐标
    ///
    /// 如果`coord`格子不存在，则返回[`None`]。
    /// 不保证返回的坐标存在。
    fn try_rotate(&mut self, coord: Coord) -> Option<Coord> {
        let dir = self.get(coord)?.rotate_clockwise();
        self.set(coord, dir);
        Some(coord + dir.into())
    }

    /// 触发连锁旋转
    ///
    /// 点击一格，顺时针旋转，并触发旋转后指向的格子。重复直到超出盘面无法点击。
    ///
    /// 返回实际旋转的次数。
    pub fn trigger_rotate(&mut self, coord: Coord) -> usize {
        let path = std::iter::successors(Some(coord), |&coord| self.try_rotate(coord)); // 从头到尾的点击路径，包括触发点和最后超出盘面没有实际点击的部分
        path.count() - 1
    }
}

impl std::fmt::Display for State {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        use std::fmt::Write;
        for line in self.iter_rect() {
            for &index in line {
                f.write_char(match index {
                    Some(index) => Dir::to_char(self.cells[index]),
                    None => ' ',
                })?;
            }
            f.write_char('\n')?;
        }
        Ok(())
    }
}

impl std::str::FromStr for State {
    type Err = char;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let columns = s
            .lines()
            .map(|s| s.chars().count())
            .max()
            .and_then(|c| c.try_into().ok())
            .unwrap_or(1.try_into().unwrap());
        let mut cells = Vec::new();
        let mut lb = Builder::new(columns);
        for line in s.lines() {
            for c in line.chars() {
                if c == ' ' {
                    lb.push_hole();
                } else {
                    let dir = Dir::try_from_char(c).ok_or(c)?;
                    lb.push_cell();
                    cells.push(dir);
                }
            }
            lb.end_line();
        }
        Ok(State {
            layout: Rc::new(lb.build()),
            cells: cells.into_boxed_slice(),
        })
    }
}

#[cfg(test)]
mod test {
    use crate::{coordinate::Coord, direction::Dir, state::State};
    use indoc::indoc;
    use proptest::{prop_assert_eq, proptest};

    #[test]
    fn simple() {
        let b: State = indoc! {"
             v^^
            ^> ^
            ^^^
            ^<^
        "}
        .parse()
        .unwrap();
        assert_eq!(b.get(Coord::new(1, 0)), Some(Dir::Down));
        assert_eq!(b.get(Coord::new(0, 1)), Some(Dir::Up));
        assert_eq!(b.get(Coord::new(1, 1)), Some(Dir::Right));
        assert_eq!(b.get(Coord::new(1, 3)), Some(Dir::Left));
        assert_eq!(b.get(Coord::new(2, 1)), None);
        assert_eq!(b.get(Coord::new(3, 3)), None);
        assert_eq!(b.get(Coord::new(-114, 514)), None);
    }

    proptest!(
        #[test]
        fn fmt(s in "[\\^v<> \n]*") {
            let b: State = s.parse().unwrap();
            prop_assert_eq!(b.to_string().parse(), Ok(b));
        }
    );
}
